package com.tony.jrp.core;

import com.tony.jrp.config.JRPLocation;
import com.tony.jrp.util.HttpUtils;
import io.vertx.core.Future;
import io.vertx.core.http.*;
import io.vertx.core.net.NetSocket;
import io.vertx.core.net.SocketAddress;
import io.vertx.core.net.impl.HostAndPortImpl;
import io.vertx.httpproxy.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class JRPReverseProxy implements HttpProxy {
    private static final Logger logger = LoggerFactory.getLogger(JRPReverseProxy.class);
    public static final String SUFFIX = "/";
    public static final String REGEX_SUFFIX = ".*";
    public final RequestOptions originRequestOptions = new RequestOptions();
    private final HttpClient client;
    private final boolean supportWebSocket;
    private BiFunction<HttpServerRequest, HttpClient, Future<HttpClientRequest>> selector = (req, client) -> Future.failedFuture("No origin available");
    private final List<ProxyInterceptor> interceptors = new ArrayList<>();
    private final JRPLocation config;
    private final URL targetUrl;

    public JRPReverseProxy(JRPLocation config, ProxyOptions options, HttpClient client) {
        try {
            targetUrl = new URL(config.getProxy_pass());
        } catch (MalformedURLException e) {
            logger.error("地址解析错误：{}", e.getMessage(), e);
            throw new RuntimeException(e);
        }
        int port = targetUrl.getPort();
        boolean httpsFlag = "https".equalsIgnoreCase(targetUrl.getProtocol());
        boolean httpFlag = "http".equalsIgnoreCase(targetUrl.getProtocol());
        if (httpsFlag && port == -1) {
            port = 443;
        } else if (httpFlag && port == -1) {
            port = 80;
        }
        this.origin(port, targetUrl.getHost());
        originRequestOptions.setSsl(httpsFlag);
        this.config = config;
        this.client = client;
        this.supportWebSocket = options.getSupportWebSocket();
    }

    public HttpProxy originSelector(Function<HttpServerRequest, Future<SocketAddress>> selector) {
        return this.originRequestProvider((req, client) -> selector.apply(req).flatMap((server) -> client.request(originRequestOptions.setServer(server))));
    }

    public HttpProxy originRequestProvider(BiFunction<HttpServerRequest, HttpClient, Future<HttpClientRequest>> provider) {
        this.selector = provider;
        return this;
    }

    public HttpProxy addInterceptor(ProxyInterceptor interceptor) {
        this.interceptors.add(interceptor);
        return this;
    }

    public void handle(HttpServerRequest request) {
        ProxyRequest proxyRequest = ProxyRequest.reverseProxy(request);
        proxyRequest.setAuthority(new HostAndPortImpl(targetUrl.getHost(), targetUrl.getPort()));
        String uri = request.uri();
        //代理路径格式可能为：http://192.168.1.111,http://192.168.1.111/,http://192.168.1.111/user?name=1
        String proxyPass = config.getProxy_pass();
        if (proxyPass.endsWith(SUFFIX)) {
            /*
             * 被代理地址以"/“结束，目标地址需要判断转换：
             * 1.如果路径正则匹配，并且".*"结尾的需要替换前缀为被代理地址的路径。
             * 2.如果路径正则匹配，并且".*"结尾，访问路径和目标路径一致。
             * 3.如果路径不是正则匹配，代理路径的前缀替换为被代理地址的路径。
             */
            String location = config.getLocation();
            String targetUrlPath = targetUrl.getPath();
            if (config.getRegex()) {
                if (location.endsWith(REGEX_SUFFIX)) {
                    //.*结尾的替换前缀为配置的目标路径
                    location = location.substring(0, location.length() - 2);
                    Pattern pattern = Pattern.compile(location);
                    Matcher matcher = pattern.matcher(uri);
                    if (matcher.find()) {
                        //如果location为：/api/user/.*，客户端请求uri为：/api/user/add，被代理原始服务路径targetUrlPath为：/cs/，替换前缀[/api/user/]后实际请求地址uri为：http://127.0.0.1/cs/add
                        uri = matcher.replaceFirst(targetUrlPath);
                    }
                }
                //不是.*结尾的正则，请求路径和目标路径一致
            } else {
                // 非正则代理路径：类似/api/user/xxx路径替换为targetUrl.getPath()/xxx
                if (!location.startsWith(SUFFIX)) {
                    location = SUFFIX + location;
                }
                if (!location.endsWith(SUFFIX)) {
                    location = location + SUFFIX;
                }
                if (uri.startsWith(location)) {
                    uri = uri.replaceFirst(location, targetUrlPath);
                }
            }
        }
        //追加地址栏固定参数
        if (targetUrl.getQuery() != null) {
            uri = uri.contains("?") ? uri + targetUrl.getQuery() : uri + "?" + targetUrl.getQuery();
        }
        proxyRequest.setURI(uri);
        Boolean chunked = HttpUtils.isChunked(request.headers());
        if (chunked == null) {
            this.end(proxyRequest, 400);
        } else if (this.supportWebSocket && io.vertx.core.http.impl.HttpUtils.canUpgradeToWebSocket(request)) {
            this.handleWebSocketUpgrade(proxyRequest);
        } else {
            JRPReverseProxy.Proxy proxy = new JRPReverseProxy.Proxy(proxyRequest);
            proxy.filters = this.interceptors.listIterator();
            proxy.sendRequest().recover((throwable) -> {
                logger.trace("Error in sending the request", throwable);
                return Future.succeededFuture(proxyRequest.release().response().setStatusCode(502));
            }).compose(proxy::sendProxyResponse).recover((throwable) -> {
                logger.trace("Error in sending the response", throwable);
                return proxy.response().release().setStatusCode(502).send();
            });
        }
    }

    /**
     * websocket请求处理
     *
     * @param proxyRequest 代理请求对象
     */
    private void handleWebSocketUpgrade(ProxyRequest proxyRequest) {
        HttpServerRequest proxiedRequest = proxyRequest.proxiedRequest();
        this.resolveOrigin(proxiedRequest).onComplete((ar) -> {
            if (ar.succeeded()) {
                HttpClientRequest request = ar.result();
                request.setMethod(HttpMethod.GET);
                request.setURI(proxyRequest.getURI());
                request.headers().addAll(proxiedRequest.headers()).set(HttpHeaders.CONNECTION, HttpHeaders.UPGRADE);
                Future<HttpClientResponse> fut2 = request.connect();
                proxiedRequest.handler(request::write);
                proxiedRequest.endHandler((v) -> request.end());
                proxiedRequest.resume();
                fut2.onComplete((ar2) -> {
                    if (ar2.succeeded()) {
                        HttpClientResponse proxiedResponse = ar2.result();
                        if (proxiedResponse.statusCode() == 101) {
                            HttpServerResponse response = proxiedRequest.response();
                            response.setStatusCode(101);
                            response.headers().addAll(proxiedResponse.headers());
                            Future<NetSocket> others = proxiedRequest.toNetSocket();
                            others.onComplete((ar3) -> {
                                if (ar3.succeeded()) {
                                    NetSocket responseSocket = ar3.result();
                                    NetSocket proxyResponseSocket = proxiedResponse.netSocket();
                                    responseSocket.handler(proxyResponseSocket::write);
                                    proxyResponseSocket.handler(responseSocket::write);
                                    responseSocket.closeHandler((v) -> proxyResponseSocket.close());
                                    proxyResponseSocket.closeHandler((v) -> responseSocket.close());
                                } else {
                                    System.err.println("Handle this case");
                                    logger.error("websocket exception:{}", ar3.cause().getMessage());
                                }
                            });
                        } else {
                            proxiedRequest.resume();
                            this.end(proxyRequest, proxiedResponse.statusCode());
                        }
                    } else {
                        proxiedRequest.resume();
                        this.end(proxyRequest, 502);
                    }

                });
            } else {
                proxiedRequest.resume();
                this.end(proxyRequest, 502);
            }

        });
    }

    private void end(ProxyRequest proxyRequest, int sc) {
        proxyRequest.response().release().setStatusCode(sc).putHeader(HttpHeaders.CONTENT_LENGTH, "0").setBody(null).send();
    }

    private Future<HttpClientRequest> resolveOrigin(HttpServerRequest proxiedRequest) {
        return this.selector.apply(proxiedRequest, this.client);
    }

    private class Proxy implements ProxyContext {
        private final ProxyRequest request;
        private ProxyResponse response;
        private final Map<String, Object> attachments;
        private ListIterator<ProxyInterceptor> filters;

        private Proxy(ProxyRequest request) {
            this.attachments = new HashMap<>();
            this.request = request;
        }

        public void set(String name, Object value) {
            this.attachments.put(name, value);
        }

        public <T> T get(String name, Class<T> type) {
            Object o = this.attachments.get(name);
            return (type.isInstance(o) ? type.cast(o) : null);
        }

        public ProxyRequest request() {
            return this.request;
        }

        public Future<ProxyResponse> sendRequest() {
            if (this.filters.hasNext()) {
                ProxyInterceptor next = this.filters.next();
                return next.handleProxyRequest(this);
            } else {
                return this.sendProxyRequest(this.request);
            }
        }

        public ProxyResponse response() {
            return this.response;
        }

        public Future<Void> sendResponse() {
            if (this.filters.hasPrevious()) {
                ProxyInterceptor filter = this.filters.previous();
                return filter.handleProxyResponse(this);
            } else {
                return this.response.send();
            }
        }

        private Future<ProxyResponse> sendProxyRequest(ProxyRequest proxyRequest) {
            Future<HttpClientRequest> requestFuture = JRPReverseProxy.this.resolveOrigin(proxyRequest.proxiedRequest());
            return requestFuture.compose(proxyRequest::send);
        }

        private Future<Void> sendProxyResponse(ProxyResponse response) {
            this.response = response;
            Boolean chunked = HttpUtils.isChunked(response.headers());
            if (chunked == null) {
                JRPReverseProxy.this.end(response.request(), 501);
                return Future.succeededFuture();
            } else {
                return this.sendResponse();
            }
        }
    }
}
